/* Copyright (c) 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Collections;
using System.Web;

namespace Google.Apps.Reporting
{
    /// <summary>
    /// This contains the logic for constructing and submitting a report 
    /// request to the Google Apps reporting service and reading the response.
    ///
    /// The description of the web service protocol can be found at:
    ///
    /// http://code.google.com/apis/apps/reporting/google_apps_reporting_api.html
    /// 
    /// Example usage:
    ///   ReportingAPIClient client = new ReportingAPIClient();
    ///   client.email = "admin@example.com";
    ///   client.password = "passwd";
    ///   client.domain = "example.com";
    ///   client.ClientLogin();
    /// Get the latest accounts report to standard output.
    ///   client.getReport("accounts", null, null);
    /// Get the accounts report for May 15, 2007 and save it to out.txt.
    ///   client.getReport("accounts", "2007-05-15", "out.txt");
    /// </summary>
    public class ReportingAPIClient
    {
        /// <summary>
        /// URL to POST to obtain an authentication token
        /// </summary>
        private const string CLIENT_LOGIN_URL =
          "https://www.google.com/accounts/ClientLogin";

        /// <summary>
        /// URL to POST to retrive resports from the API 
        /// </summary>
        private const string REPORTING_URL =
          "https://www.google.com/hosted/services/v1.0/reports/ReportingData";

        /// <summary>
        /// Date format of the API
        /// </summary>
        private const string DATE_FORMAT = "yyyy-MM-dd";

        /// <summary>
        /// Hour of the day when the API data gets published
        /// </summary>
        private const int PUBLISH_HOUR_OF_DAY = 13; // Publish hour + 1 hour;

        /// <summary>
        /// Time diference to UTC
        /// </summary>
        private const int PUBLISH_TIME_DIFERENCE_TO_UTC = -8;

        /// <summary>
        /// Email command-line argument
        /// </summary>
        private const string EMAIL_ARG = "email";

        /// <summary>
        /// Password command-line argument
        /// </summary>
        private const string PASSWORD_ARG = "password";

        /// <summary>
        /// Domain command-line argument
        /// </summary>
        private const string DOMAIN_ARG = "domain";

        /// <summary>
        /// Report command-line argument
        /// </summary>
        private const string REPORT_ARG = "report";

        /// <summary>
        /// Date command-line argument
        /// </summary>
        private const string DATE_ARG = "date";

        /// <summary>
        /// Output File command-line argument
        /// </summary>
        private const string OUT_FILE_ARG = "out";

        /// <summary>
        /// Message for command-line usage
        /// </summary>
        private const string USAGE = "Usage:  " +
          "ReportingAPI --" + EMAIL_ARG + " <email> --" +
          PASSWORD_ARG + " <password> [ --" + 
          DOMAIN_ARG + " <domain> ] --" +
          REPORT_ARG + " <report name> [ --" + 
          DATE_ARG + " <YYYY-MM-DD> ] [ --" +
          OUT_FILE_ARG + " <file name> ]";

        /// <summary>
        /// List of command-line arguments
        /// </summary>
        private static string[] PROPERTY_NAMES = new String[] {EMAIL_ARG,
          PASSWORD_ARG, DOMAIN_ARG, REPORT_ARG, DATE_ARG, OUT_FILE_ARG};

        /// <summary>
        /// List of required command-line arguments
        /// </summary>
        private static string[] REQUIRED_PROPERTY_NAMES = new String[] {
          EMAIL_ARG, PASSWORD_ARG, REPORT_ARG};

        /// <summary>
        /// Google Apps Domain
        /// </summary>
        public string domain = null;

        /// <summary>
        /// Email address of an Administrator account
        /// </summary>
        public string email = null;

        /// <summary>
        /// Password of the Administrator account
        /// </summary>
        public string password = null;

        /// <summary>
        /// Identifies the type of account
        /// </summary>
        private string accountType = "HOSTED";

        /// <summary>
        /// Identifies the Google service
        /// </summary>
        private string service = "apps";

        /// <summary>
        /// Contains a token value that Google uses to authorize 
        /// access to the requested report data
        /// </summary>
        private string token = null;

        /// <summary>
        /// Default constructor
        /// </summary>
        public ReportingAPIClient()
        {
        }

        /// <summary>
        /// Retrieves the Authentication Token
        /// </summary>
        /// <returns>Returns the authentication token.</returns>
        public string GetToken()
        {
            return this.token;
        }

        /// <summary>
        /// Logs in the user and initializes the Token
        /// </summary>
        public void ClientLogin()
        {
            string token = null;
            UTF8Encoding encoding = new UTF8Encoding();
            string postData = "Email=" + HttpUtility.UrlEncode(this.email) +
              "&Passwd=" + HttpUtility.UrlEncode(this.password) +
              "&accountType=" + HttpUtility.UrlEncode(this.accountType) +
              "&service=" + HttpUtility.UrlEncode(this.service);
            byte[] data = encoding.GetBytes(postData);
            HttpWebRequest request =
              (HttpWebRequest)WebRequest.Create(CLIENT_LOGIN_URL);
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            request.ContentLength = data.Length;
            Stream inputStream = request.GetRequestStream();
            inputStream.Write(data, 0, data.Length);
            inputStream.Close();
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            string responseStr = (new StreamReader(
              response.GetResponseStream())).ReadToEnd();
            char[] tokenizer = { '\r', '\n' };
            string[] parts = responseStr.Split(tokenizer);
            foreach (string part in parts)
            {
                if (part.StartsWith("SID="))
                {
                    token = part.Substring(4);
                    break;
                }
            }
            this.token = token;
        }

        /// <summary>
        /// Creates a XML request for the Report
        /// </summary>
        /// <param name="reportName">The name of the Report: activity, 
        ///   disk_space, email_clients, quota_limit_accounts, 
        ///   summary, suspended_account</param>
        /// <param name="date">Date of the Report</param>
        /// <returns>Thx XML request as a string</returns>
        public string createRequestXML(string reportName, string date)
        {
            if (this.domain == null)
            {
                this.domain = getAdminEmailDomain();
            }
            string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
            xml += "<rest xmlns=\"google:accounts:rest:protocol\"";
            xml += " xmlns:xsi=\"";
            xml += "http://www.w3.org/2001/XMLSchema-instance\">";
            xml += "<type>Report</type>";
            xml += "<token>" + this.GetToken() + "</token>";
            xml += "<domain>" + this.domain + "</domain>";
            xml += "<date>" + date + "</date>";
            xml += "<reportType>daily</reportType>";
            xml += "<reportName>" + reportName + "</reportName>";
            xml += "</rest>";
            return xml;
        }

        /// <summary>
        /// Get the domain of the admin's email address.
        /// </summary>
        /// <returns>the domain, otherwise returns null</returns>
        public string getAdminEmailDomain()
        {
            if (this.email != null)
            {
                int atIndex = this.email.IndexOf('@');
                if (atIndex > 0 && atIndex + 1 < this.email.Length)
                {
                    return this.email.Substring(atIndex + 1);
                }
            }
            else
            {
                throw new ArgumentNullException("Invalid Email");
            }
            return null;
        }

        /// <summary>
        /// Get the reports by reportName for a Date, and writes the report
        /// at filename         /// or to the console if filename is null.
        /// </summary>
        /// <param name="reportName">The name of the Report: activity, 
        ///   disk_space, email_clients, quota_limit_accounts,summary,
        ///   suspended_account</param>
        /// <param name="date">Date of the Report, 
        ///   null date gets latest date available</param>
        /// <param name="fileName">file name to write the 
        ///   report or null for console</param>
        public void getReport(string reportName, string date, string fileName)
        {
            if (date == null)
            {
                date = getLatestReportDate().ToString(DATE_FORMAT);
            }
            else
            {
                try
                {
                    date = System.Convert.ToDateTime(date).ToString
                      (DATE_FORMAT);
                }
                catch
                {
                    throw new ArgumentException("Invalid Date");
                }
            }
            string xml = createRequestXML(reportName, date);
            HttpWebRequest request =
              (HttpWebRequest)WebRequest.Create(REPORTING_URL);
            request.Method = "POST";
            UTF8Encoding encoding = new UTF8Encoding();
            byte[] postBuffer = encoding.GetBytes(xml);
            request.ContentLength = postBuffer.Length;
            request.ContentType = "application/x-www-form-urlencoded";
            Stream requestStream = request.GetRequestStream();
            requestStream.Write(postBuffer, 0, postBuffer.Length);
            requestStream.Close();
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            StreamReader reader = new StreamReader(
              response.GetResponseStream());
            String firstLine = null;
            String lineBuffer = String.Empty;
            if (reader.Peek() >= 0)
            {
                firstLine = reader.ReadLine();
                checkError(firstLine, reader);
            }

            if (fileName != null)
            {
                StreamWriter streamWriter = null;
                streamWriter = new StreamWriter(fileName);
                streamWriter.WriteLine(firstLine);
                while ((lineBuffer = reader.ReadLine()) != null)
                {
                    streamWriter.WriteLine(lineBuffer);
                }
                streamWriter.Close();
            }

            else
            {
                if (firstLine != null)
                {
                    Console.WriteLine(firstLine);
                }
                while ((lineBuffer = reader.ReadLine()) != null)
                {
                    Console.WriteLine(lineBuffer);
                }
            }
            reader.Close();
        }

        /// <summary>
        /// Get the reports by reportName for a Date, and writes the report 
        /// at filename or to the console if filename is null.
        /// </summary>
        /// <param name="reportName">The name of the Report: activity, 
        ///   disk_space, email_clients, quota_limit_accounts,summary,
        ///   suspended_account</param>
        /// <param name="date">
        ///   Date of the Report, null date gets latest date available</param>
        /// <param name="fileName">
        ///   file name to write the report or null for console</param>
        public void getReport(string reportName, 
          DateTime date, string fileName)
        {
            this.getReport(reportName, date.ToString(DATE_FORMAT), fileName);
        }

        /// <summary>
        /// Checks for errors on the Http Response, errors are on XML format.
        /// When the response is xml throws an Exception with the xml text.
        /// </summary>
        /// <param name="firstLine">
        ///   First line of the StreamReader from the Http Response</param>
        /// <param name="reader">StreamReader from the Http Response</param>
        private void checkError(string firstLine, StreamReader reader)
        {
            if (firstLine.Trim().StartsWith("<?xml"))
            {
                String xmlText = firstLine;
                while (reader.Peek() >= 0)
                {
                    xmlText += reader.ReadLine();
                }
                throw new Exception(xmlText);
            }
        }

        /// <summary>
        /// Get latest available report date, 
        /// based on report service time zone.
        /// Reports for the current date are available after 12:00 PST8PDT 
        /// the following day.
        /// </summary>
        /// <returns>Lastest date available</returns>
        public DateTime getLatestReportDate()
        {
            if (DateTime.UtcNow.AddHours(PUBLISH_TIME_DIFERENCE_TO_UTC).Hour
              < PUBLISH_HOUR_OF_DAY)
            {
                return DateTime.Now.AddDays(-2);
            }
            else
            {
                return DateTime.Now.AddDays(-1);
            }
        }

        /// <summary>
        /// Gets the properties from the command-line arguments.
        /// </summary>
        /// <param name="args">command-line arguments</param>
        /// <returns>Properties Hashtable</returns>
        private static Hashtable getProperties(string[] args)
        {
            Hashtable properties = new Hashtable();
            for (int i = 0; i < args.Length; i++)
            {
                bool found = false;
                for (int j = 0; j < PROPERTY_NAMES.Length; j++)
                {
                    if (args[i].Equals("--" + PROPERTY_NAMES[j]))
                    {
                        found = true;
                        if (i + 1 < args.Length)
                        {
                            properties.Add(PROPERTY_NAMES[j], args[i + 1]);
                            i++;
                            break;
                        }
                        else
                        {
                            throw new ArgumentException("Missing value for " +
                              "command-line parameter " + args[i]);
                        }
                    }
                }
                if (!found)
                {
                    throw new ArgumentException(
                      "Unrecognized parameter " + args[i]);
                }
            }
            for (int i = 0; i < REQUIRED_PROPERTY_NAMES.Length; i++)
            {
                if (properties[REQUIRED_PROPERTY_NAMES[i]] == null)
                {
                    throw new ArgumentException("Missing value for " +
                      "command-line parameter " + REQUIRED_PROPERTY_NAMES[i]);
                }
            }
            return properties;
        }

        /// <summary>
        ///  Request a report based on command-line arguments.
        /// </summary>
        /// <param name="args">command-line arguments</param>
        public static void Main(string[] args)
        {
            Hashtable props = null;
            try
            {
                props = getProperties(args);
            }
            catch (ArgumentException e)
            {
                Console.WriteLine(e.Message);
                Console.WriteLine(USAGE);
                return;
            }
            try
            {
                ReportingAPIClient client = new ReportingAPIClient();
                client.email = (string)props[EMAIL_ARG];
                client.password = (string)props[PASSWORD_ARG];
                client.domain = (string)props[DOMAIN_ARG];
                client.ClientLogin();
                client.getReport((string)props[REPORT_ARG],
                  (string)props[DATE_ARG], (string)props[OUT_FILE_ARG]);
            }
            catch (Exception e)
            {
                Console.WriteLine("Operation failed: {0}", e.Message);
            }
        }
    }
}